﻿using System;
using System.Collections;
using System.Data.Spatial;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Formatting;
using System.Net.Http.Headers;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;

namespace WAPISF
{
    /// <summary>
    /// 
    /// </summary>
    public class GeoJsonFormatter : MediaTypeFormatter
    {
        private readonly string geoJson = "application/vnd.geofeatures+json";       // to support a media type built on geojson
        private readonly string json = "application/json";

        public GeoJsonFormatter()
        {
            SupportedMediaTypes.Add(new MediaTypeHeaderValue(json));
            SupportedMediaTypes.Add(new MediaTypeHeaderValue(geoJson));
        }

        public override bool CanWriteType(Type type)
        {
            if (type == null) throw new ArgumentNullException("type is null");
            return true;
        }

        public override bool CanReadType(Type type)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// Called during deserialization to read an object of the specified <paramref name="type"/>
        /// from the specified <paramref name="stream"/>.
        /// </summary>
        /// <param name="type">The type of object to read.</param>
        /// <param name="stream">The <see cref="Stream"/> from which to read.</param>
        /// <param name="content">The <see cref="HttpContent"/> for the content being written.</param>
        /// <param name="formatterLogger">The <see cref="IFormatterLogger"/> to log events to.</param>
        /// <returns>A <see cref="Task"/> whose result will be the object instance that has been read.</returns>
        public override Task<object> ReadFromStreamAsync(Type type, Stream readStream, HttpContent content, IFormatterLogger formatterLogger)
        {
            if (type == null)
            {
                throw new ArgumentNullException("type");
            }

            if (readStream == null)
            {
                throw new ArgumentNullException("stream");
            }

            return Task.Factory.StartNew<object>(() =>
            {
                // If content length is 0 then return default value for this type
                if (content != null && content.Headers.ContentLength == 0)
                {
                    return GetDefaultValueForType(type);
                }

                // get an instance of the class and setup a list to 1 or more of then
                var s = Activator.CreateInstance(type);
                var featureCollection = (new[] { s }).ToList();

                string propertyName = "";
                PropertyInfo pinfo = null;

                using (var streamReader = new StreamReader(readStream))
                using (var jsonReader = new JsonTextReader(streamReader))
                {
                    while (jsonReader.Read())
                    {
                        switch (jsonReader.TokenType)
                        {
                            case JsonToken.PropertyName:
                                Debug.WriteLine("PropertyName " + jsonReader.Value.ToString());
                                propertyName = jsonReader.Value.ToString();
                                break;

                            case JsonToken.StartConstructor:
                            case JsonToken.EndConstructor:
                                continue;

                            case JsonToken.StartObject:
                                //sb.Append("{");
                                break;


                            case JsonToken.StartArray:
                                //sb.Append("[");
                                break;

                            case JsonToken.EndObject:
                                //sb.Append("}");
                                break;

                            case JsonToken.EndArray:
                                //sb.Append("]");
                                break;

                            case JsonToken.Boolean:
                                //sb.Append(reader.Value.ToString().ToLowerInvariant());
                                break;

                            case JsonToken.Date:
                                //sb.Append(JsonConvert.SerializeObject(reader.Value));
                                break;

                            case JsonToken.Bytes:
                            case JsonToken.Comment:
                            case JsonToken.Float:
                            case JsonToken.Integer:
                                Debug.WriteLine("Bytes/Comment/Float/Integer " + jsonReader.Value);
                                pinfo = type.GetProperty(propertyName);
                                if(pinfo != null)
                                    pinfo.SetValue(s, jsonReader.Value as Int16?, null);
                                break;

                            case JsonToken.Null:
                                //sb.Append(reader.Value.ToString().ToLowerInvariant());
                                break;

                            case JsonToken.String:
                                Debug.WriteLine("String " + jsonReader.Value);
                                pinfo = type.GetProperty(propertyName);
                                if(pinfo != null)
                                    pinfo.SetValue(s, jsonReader.Value);
                                break;

                            case JsonToken.Raw:
                                //sb.Append(reader.Value);
                                break;

                            default:
                                break;
                        }
                        //if (jsonReader.TokenType == JsonToken.StartObject)
                        //{
                        //    JObject tbox = JObject.Load(jsonReader);
                        //}
                     }
                }
                return featureCollection;
            });
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="type"></param>
        /// <param name="value"></param>
        /// <param name="stream"></param>
        /// <param name="content"></param>
        /// <param name="transportContext"></param>
        /// <returns></returns>
        public override Task WriteToStreamAsync(Type type, object value, Stream stream, HttpContent content, TransportContext transportContext)
        {
            if (type == null)
            {
                throw new ArgumentNullException("type");
            }

            if (stream == null)
            {
                throw new ArgumentNullException("stream");
            }

            return Task.Factory.StartNew(() =>
            {
                BuildGeoJsonFeed(value, stream, content.Headers.ToString());
            });

        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="models"></param>
        /// <param name="stream"></param>
        /// <param name="contenttype"></param>
        private void BuildGeoJsonFeed(object models, Stream stream, string contenttype)
        {
            JsonSerializerSettings _jsonSerializerSettings;
            _jsonSerializerSettings = new JsonSerializerSettings();

            // Create a serializer
            JsonSerializer serializer = JsonSerializer.Create(_jsonSerializerSettings);

            using (JsonTextWriter jsonTextWriter = new JsonTextWriter(new StreamWriter(stream, Encoding.UTF8)) { CloseOutput = false })
            {
                string geomPropertyName = "";

                if (models is IEnumerable)
                {
                    // get properties from first object
                    object first = null;
                    foreach (object o in (models as IEnumerable))
                    {
                        first = o;
                        break;
                    }

                    var properties = first.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(p =>
                            p.Name != "EntityKey" && p.Name != "EntityState" && p.PropertyType.BaseType.Name != "EntityReference");

                    // discover the geometry property and get CRS of first object
                    foreach (var property in properties)
                    {
                        if (property.PropertyType.Name == "DbGeometry")
                        {
                            geomPropertyName = property.Name;
                            break;
                        }
                    }

                    jsonTextWriter.WriteStartObject();
                    jsonTextWriter.WritePropertyName("type");
                    jsonTextWriter.WriteValue("FeatureCollection");
                    JsonSpatialConverter jsc = new JsonSpatialConverter();
                    jsc.WriteCrs(jsonTextWriter, first.GetType().GetProperty(geomPropertyName).GetValue(first) as DbGeometry);
                    jsonTextWriter.WritePropertyName("features");
                    jsonTextWriter.WriteStartArray();

                    foreach (object o in (models as IEnumerable))
                    {
                        jsonTextWriter.WriteStartObject();
                        jsonTextWriter.WritePropertyName("type");
                        jsonTextWriter.WriteValue("Feature");

                        jsonTextWriter.WritePropertyName("geometry");
                        //JsonSpatialConverter jsc = new JsonSpatialConverter();
                        jsc.WriteJson(jsonTextWriter, o.GetType().GetProperty(geomPropertyName).GetValue(o), serializer);

                        // write all non-spatial properties
                        jsonTextWriter.WritePropertyName("properties");
                        jsonTextWriter.WriteStartObject();

                        foreach (var property in properties)
                        {
                            // primative and string properties
                            if (property.PropertyType.IsPrimitive || property.PropertyType.Name == "String")
                            {
                                Debug.WriteLine(string.Format("Property: {0}, Type: {1}, value {2}", property.Name, property.PropertyType.Name, o.GetType().GetProperty(property.Name).GetValue(o)));

                                jsonTextWriter.WritePropertyName(property.Name);
                                jsonTextWriter.WriteValue(o.GetType().GetProperty(property.Name).GetValue(o));
                                
                            }
                            // Navigation property for EntityObject
                            else if (property.PropertyType.BaseType.Name == "EntityObject")
                            {
                                // don't do anything 
                            }
                            else if (property.PropertyType.IsGenericType)
                            {
                                // need to handle Nullable types
                            }
                            else
                            {
                                // need to handle complex types too (ignore spatial types here too!)
                                Debug.WriteLine(string.Format("Complex Property: {0}, Type: {1}", property.Name, property.PropertyType.Name));
                            }
                        }
                        jsonTextWriter.WriteEndObject();
                        jsonTextWriter.WriteEndObject();
                    }
                    jsonTextWriter.WriteEndArray();
                }
                jsonTextWriter.WriteEndObject();  // end FeatureCollection
                jsonTextWriter.Flush();
            }
        }
    }
}